class Axes {

  // private
  float x0,y0,wd,ht; // sreen location
  float xdmin=0, xdmax=1, ydmin=0, ydmax=1, zdmin=0, zdmax=1; // data ranges
  String topLabel, bottomLabel, leftLabel, rightLabel;
  boolean largeTopLabel = false;
  String[] topTickLabels, bottomTickLabels, leftTickLabels, rightTickLabels;
  float[] topTicks, bottomTicks, leftTicks, rightTicks;
  float leftLabelOffset=0, rightLabelOffset=0, topLabelOffset=0, bottomLabelOffset=0;
  color col = color(0);
  float fontSize = 12;
  AxesCell[][] cells;
  
  // public
  float[] horizGrid, vertGrid;
  boolean gridOn = true, gridOnTop = true;
  boolean axesOn = true;
  boolean labelsOn = true, labelsOnTop = true;
  color bkgndColor = color(255,0);
  color gridColor = color(255);
  color[] colormap;
  
  // style info
  Stylesheet styles = masterStylesheet; // really this should be inherited
  
  Axes() {}
  
  Axes(float[] rect) {
    setPosition(rect[0], rect[1], rect[2], rect[3]);
  }
  
  Axes(float x0, float y0, float wd, float ht) {
    setPosition(x0,y0,wd,ht);
  }
  
  void setPosition(float x0, float y0, float wd, float ht) {
    this.x0 = x0;
    this.y0 = y0;
    this.wd = wd;
    this.ht = ht;
  }
  
  void setAppearance(color col, float fontSize) {
    this.col = col;
    this.fontSize = fontSize;
  }

  void setXRange(float xdmin, float xdmax) {
    this.xdmin = xdmin;
    this.xdmax = xdmax;
  }
  
  void setYRange(float ydmin, float ydmax) {
    this.ydmin = ydmin;
    this.ydmax = ydmax;
  }

  void setZRange(float zdmin, float zdmax) {
    this.zdmin = zdmin;
    this.zdmax = zdmax;
  }


  void setLabels(String where, String label) {
    if (where.equals("left") || where.equals("left of origin") || where.equals("right") || where.equals("right of origin")) {
      setLabels(where, label, horizGrid);
    } else {
      setLabels(where, label, vertGrid);
    }
  }

  void setLabels(String where, String label, float[] ticks) {
    String[] tickLabels = null;
    if (ticks != null) {
      tickLabels = new String[ticks.length];
      for (int i=0; i<ticks.length; i++) tickLabels[i] = nicenum(ticks[i]);
    }
    setLabels(where, label, ticks, tickLabels);
  }
  
  void setLabels(String where, String label, float[] ticks, String[] tickLabels) {
    if (where.equals("left")) {
      leftLabel = label;
      leftTicks = ticks;
      leftTickLabels = tickLabels;
    } else if (where.equals("right")) {
      rightLabel = label;
      rightTicks = ticks;
      rightTickLabels = tickLabels;
    } else if (where.equals("top")) {
      topLabel = label;
      topTicks = ticks;
      topTickLabels = tickLabels;
    } else if (where.equals("bottom")) {
      bottomLabel = label;
      bottomTicks = ticks;
      bottomTickLabels = tickLabels;
    } else if (where.equals("left of origin")) {
      setLabels("left", label, ticks, tickLabels);
      leftLabelOffset = x2scr(0) - left();
    } else if (where.equals("right of origin")) {
      setLabels("right", label, ticks, tickLabels);
      rightLabelOffset = x2scr(0) - right();
    } else if (where.equals("top of origin")) {
      setLabels("top", label, ticks, tickLabels);
      topLabelOffset = y2scr(0) - top();
    } else if (where.equals("bottom of origin")) {
      setLabels("bottom", label, ticks, tickLabels);
      bottomLabelOffset = y2scr(0) - bottom();
    }
  }
  
  float left() {return x0;}
  float right() {return x0+wd;}
  float top() {return y0;}
  float bottom() {return y0+ht;}
  float width() {return right()-left();}
  float height() {return bottom()-top();}
  
  float xmin() {return xdmin;}
  float xmax() {return xdmax;}
  float ymin() {return ydmin;}
  float ymax() {return ydmax;}
  float zmin() {return zdmin;}
  float zmax() {return zdmax;}
  
  float scr2x(float s) {return map(s, left(), right(), xmin(), xmax());}
  float scr2y(float s) {return map(s, bottom(), top(), ymin(), ymax());}
  float x2scr(float x) {return map(x, xmin(), xmax(), left(), right());}
  float y2scr(float y) {return map(y, ymin(), ymax(), bottom(), top());}
  
  float mouseXd() {return scr2x(mouseX);}
  float mouseYd() {return scr2y(mouseY);}

  boolean over() {
    return (mouseX >= left() && mouseX <= right() && mouseY >= top() && mouseY <= bottom());
  }
  
  int[] overWhich() {
    if (cells==null) return new int[] {-1, -1};
    for (int j=0; j<cells.length; j++) {
      for (int i=0; i<cells[0].length; i++) {
        if (cells[j][i].over()) return new int[] {j, i};
      }
    }
    return new int[] {-1, -1};
  }
  
  void drawAxes() {drawAxes(col,1);}
  void drawAxes(color col, float wt) {
    pushStyle();
    stroke(col);
    strokeWeight(wt);
    fill(bkgndColor);
    rectMode(CORNERS);
    rect(left(), top(), right(), bottom());
    popStyle();
  }
  
  void drawLabels() {drawLabels(col,fontSize);}
  
  void drawLabels(color col, float fontSize) {
    float sp = fontSize/4;
    pushStyle();
    fill(col);
    styles.setFont(fontSize);    
    if (leftLabel != null || leftTickLabels != null) {
      textAlign(RIGHT);
      float x = left() + leftLabelOffset - sp;
      float maxWd = 0;
      if (leftTickLabels != null) {
        for (int i=0; i<leftTickLabels.length; i++) {
          textVAlign(leftTickLabels[i], x, y2scr(leftTicks[i]), "middle");
          maxWd = max(maxWd, textWidth(leftTickLabels[i]));
        }
        x -= maxWd - sp;
      }
      if (leftLabel != null) {
        pushMatrix();
        translate(x, 0.5*(top()+bottom()));
        rotate(HALF_PI);
        textAlign(CENTER);
        text(leftLabel);
        popMatrix();
      }
    }
    if (rightLabel != null || rightTickLabels != null) {
      textAlign(LEFT);
      float x = right() + rightLabelOffset + sp;
      float maxWd = 0;
      if (rightTickLabels != null) {
        for (int i=0; i<rightTickLabels.length; i++) {
          textVAlign(rightTickLabels[i], x, y2scr(rightTicks[i]), "middle");
          maxWd = max(maxWd, textWidth(rightTickLabels[i]));
        }
        x += maxWd + sp;
      }
      if (rightLabel != null) {
        pushMatrix();
        translate(x, 0.5*(top()+bottom()));
        rotate(-HALF_PI);
        textAlign(CENTER);
        text(rightLabel);
        popMatrix();
      }
    }
    if (topLabel != null || topTickLabels != null) {
      textAlign(CENTER);
      float y = top() + topLabelOffset - sp;
      if (topTickLabels != null) {
        for (int i=0; i<topTickLabels.length; i++) {
          text(topTickLabels[i], x2scr(topTicks[i]), y);
        }
        y -= (textAscent() + sp);
      }
      if (topLabel != null) {
        titleText(topLabel, 0.5*(left()+right()), y); // mechanism for extending classes to write exciting titles
      }
    }
    if (bottomLabel != null || bottomTickLabels != null) {
      textAlign(CENTER);
      float y = bottom() + bottomLabelOffset + sp;
      if (bottomTickLabels != null) {
        for (int i=0; i<bottomTickLabels.length; i++) {
          textVAlign(bottomTickLabels[i], x2scr(bottomTicks[i]), y, "top");
        }
        y += (textAscent() + sp);
      }
      if (bottomLabel != null) {
        text(bottomLabel, 0.5*(left()+right()), y);
      }
    }
    popStyle();
  } // drawLabels
  
  void titleText(String S, float x, float y) {
    textAlign(CENTER);
    if (largeTopLabel) styles.setFont(styles.largeFontSize); // this breaks the independence of the Axes class from the font handling in this applet
    text(S,x,y);
    styles.setFont(styles.normalFontSize);
  }
  
  void drawGrid() {drawGrid(gridColor,0.5);}
  void drawGrid(color col, float wt) {
    pushStyle();
    stroke(col);
    strokeWeight(wt);
    if (horizGrid != null) {
      for (int i=0; i<horizGrid.length; i++) line(left(), y2scr(horizGrid[i]), right(), y2scr(horizGrid[i]));
    }
    if (vertGrid != null) {
      for (int i=0; i<vertGrid.length; i++) line(x2scr(vertGrid[i]), top(), x2scr(vertGrid[i]), bottom());
    }
    popStyle();
  }

  void draw() {
    if (gridOn && !gridOnTop) drawGrid();
    if (labelsOn && !labelsOnTop) drawLabels();
    if (cells != null) drawCells();
  }
  
  void drawCells() {
    for (int j=0; j<cells.length; j++) {
      for (int i=0; i<cells[0].length; i++) {
        cells[j][i].draw();
      }
    }
    for (int j=0; j<cells.length; j++) {
      for (int i=0; i<cells[0].length; i++) {
        AxesCell c = cells[j][i];
        if (c.selectable && (c.over() || c.alwaysAwake || c.pressed)) {
          pushStyle();
          noFill();
          strokeWeight(2);
          if (c.pressed) {
            stroke(127);
          } else {
            stroke(255);
          }
          rectMode(CORNERS);
          rect(c.left(), c.bottom(), c.right(), c.top());
          popStyle();
        }
      }
    }
  }
  
  void drawOnTop() {
    if (gridOn && gridOnTop) drawGrid();
    if (axesOn) drawAxes();
    if (labelsOn && labelsOnTop) drawLabels();
  }
  
  
  void plot(float[] X, float[] Y, color col, String lineSpec, float sz) {
    if (lineSpec.equals("o-") || lineSpec.equals("-o")) {
      plot(X,Y,col,"-",sz/4);
      plot(X,Y,col,"o",sz);
    } else {
      stroke(col);
      strokeWeight(sz);
      noFill();
      if (lineSpec.equals("o")) {beginShape(POINTS);} else {beginShape();}
      for (int i=0; i<X.length; i++) vertex(x2scr(X[i]), y2scr(Y[i]));
      endShape();
    }
  }
  
  void createCells(int Nj, int Ni) { // create Nj x Ni cells, which by default fill the axes. They can be moved and mapped to a colormap later.
    cells = new AxesCell[Nj][Ni];
    float dy = (ymax()-ymin()) / Nj;
    float dx = (xmax()-xmin()) / Ni;
    for (int j=0; j<Nj; j++) {
      for (int i=0; i<Ni; i++) {
        cells[j][i] = new AxesCell(this);
        cells[j][i].setXinDataUnits(xmin() + i*dx, xmin() + (i+1)*dx);
        cells[j][i].setYinDataUnits(ymin() + j*dy, ymin() + (j+1)*dy);
      }
    }
  }
  
  void createCells(float[] Xu, float[] Zw) { // create cells based on their edge coordinates
    cells = new AxesCell[Zw.length-1][Xu.length-1];
    for (int j=0; j<Zw.length-1; j++) {
      for (int i=0; i<Xu.length-1; i++) {
        cells[j][i] = new AxesCell(this);
        cells[j][i].setXinDataUnits(Xu[i], Xu[i+1]);
        cells[j][i].setYinDataUnits(Zw[j], Zw[j+1]);
      }
    }
  }

  
}





class AxesCell extends Button {

  Axes ax;
  color faceCol = color(127), edgeCol = color(255,0);
  float edgeWd = 0;
  color[] colormap;
  float zdmin, zdmax;
  boolean selectable = false;
  
  AxesCell(Axes ax) {
    this.ax = ax;
  }
  
  void setXinDataUnits(float xdmin, float xdmax) { // use these to set ranges in data units
    x0 = floor(ax.x2scr(xdmin));
    width = ceil(ax.x2scr(xdmax)) - x0;
  }
  
  void setYinDataUnits(float ydmin, float ydmax) {
    y0 = floor(ax.y2scr(ydmax));
    height = ceil(ax.y2scr(ydmin)) - y0;
  }
  
  void mapColor(float zdmin, float zdmax, color[] colormap) {
    this.zdmin = zdmin;
    this.zdmax = zdmax;
    this.colormap = colormap;
  }
  
  color getMappedColor(float z) {
    if (colormap==null) {
      return edgeCol;
    } else {
      float ind = (z-zdmin) / (zdmax-zdmin) * colormap.length; // fractional index
      int i0 = constrain(floor(ind), 0, colormap.length-1);
      int i1 = constrain(ceil(ind), 0, colormap.length-1);
      return lerpColor(colormap[i0],colormap[i1],ind-i0);
    }
  }
  
  void draw(float z) {draw(getMappedColor(z));}
  
  void draw() {draw(faceCol);}
  
  void draw(color col) {
    pushStyle();
    fill(col);
    if (edgeWd==0) {
      noStroke();
    } else {
      stroke(edgeCol);
      strokeWeight(edgeWd);
    }
    rectMode(CORNERS);
    rect(left(),bottom(),right(),top());
    popStyle();
  }
  
  boolean over() {
    return mouseX > left()  &&  mouseX < right()  &&  mouseY > top()  &&  mouseY < bottom();
  }

  
}

